package de.mmis.core.tuplespace;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.TypeVariable;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.TimeoutException;

import org.apache.log4j.Logger;

import de.mmis.core.base.Annotations;
import de.mmis.core.base.ProxyException;
import de.mmis.core.base.Toolbox;
import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.Tree.Type;
import de.mmis.core.base.abstracttree.serializer.SerializableNetworkInvocationHandler;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper;
import de.mmis.core.base.event.GenericObservable;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;
import de.mmis.core.base.gclass.GClass;
import de.mmis.core.pojop.NetworkProxy;
import de.mmis.core.pojop.NetworkPublisher;

public class TuplespaceProxy {

	private final static Logger LOGGER = Logger
			.getLogger(TuplespaceProxy.class);

	private TuplespaceProxy() {
		// hide constructor
	}

	public static <T> T createProxy(Class<T> interfaze, String id, String host,
			int port) throws UnknownHostException, IOException,
			TuplespaceException, TimeoutException {
		return createProxy(interfaze, null, new Class[] { interfaze }, id,
				host, port, true, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(Class<T> interfaze, String id,
			Tuplespace space) throws UnknownHostException, IOException,
			TuplespaceException, TimeoutException {
		return createProxy(interfaze, null, new Class[] { interfaze }, id,
				space, true, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(ClassLoader classLoader,
			Class<T> interfaze, String id, String host, int port)
			throws UnknownHostException, IOException, TuplespaceException,
			TimeoutException {
		return createProxy(interfaze, classLoader, new Class[] { interfaze },
				id, host, port, true, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(ClassLoader classLoader,
			Class<T> interfaze, String id, Tuplespace space)
			throws UnknownHostException, IOException, TuplespaceException,
			TimeoutException {
		return createProxy(interfaze, classLoader, new Class[] { interfaze },
				id, space, true, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static Proxy createProxy(String id, ClassLoader classLoader,
			Class<?>[] interfaces, String host, int port)
			throws UnknownHostException, IOException, TuplespaceException,
			TimeoutException {
		return createProxy(Proxy.class, classLoader, interfaces, id, host,
				port, true, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static Proxy createProxy(String id, ClassLoader classLoader,
			Class<?>[] interfaces, Tuplespace space)
			throws UnknownHostException, IOException, TuplespaceException,
			TimeoutException {
		return createProxy(Proxy.class, classLoader, interfaces, id, space,
				true, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(Class<T> interfaze, String id, String host,
			int port, boolean explicit) throws UnknownHostException,
			IOException, TuplespaceException, TimeoutException {
		return createProxy(interfaze, null, new Class[] { interfaze }, id,
				host, port, explicit, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(Class<T> interfaze, String id,
			Tuplespace space, boolean explicit) throws UnknownHostException,
			IOException, TuplespaceException, TimeoutException {
		return createProxy(interfaze, null, new Class[] { interfaze }, id,
				space, explicit, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(ClassLoader classLoader,
			Class<T> interfaze, String id, String host, int port,
			boolean explicit) throws UnknownHostException, IOException,
			TuplespaceException, TimeoutException {
		return createProxy(interfaze, classLoader, new Class[] { interfaze },
				id, host, port, explicit, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(ClassLoader classLoader,
			Class<T> interfaze, String id, Tuplespace space, boolean explicit)
			throws UnknownHostException, IOException, TuplespaceException,
			TimeoutException {
		return createProxy(interfaze, classLoader, new Class[] { interfaze },
				id, space, explicit, TuplespaceContainer.DEFAULT_TIMEOUT,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(Class<T> interfaze, String id, String host,
			int port, boolean explicit, long timeout)
			throws UnknownHostException, IOException, TuplespaceException,
			TimeoutException {
		return createProxy(interfaze, null, new Class[] { interfaze }, id,
				host, port, explicit, timeout,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(Class<T> interfaze, String id,
			Tuplespace space, boolean explicit, long timeout)
			throws UnknownHostException, IOException, TuplespaceException,
			TimeoutException {
		return createProxy(interfaze, null, new Class[] { interfaze }, id,
				space, explicit, timeout,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(ClassLoader classLoader,
			Class<T> interfaze, String id, String host, int port,
			boolean explicit, long timeout) throws UnknownHostException,
			IOException, TuplespaceException, TimeoutException {
		return createProxy(interfaze, classLoader, new Class[] { interfaze },
				id, host, port, explicit, timeout,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	public static <T> T createProxy(ClassLoader classLoader,
			Class<T> interfaze, String id, Tuplespace space, boolean explicit,
			long timeout) throws UnknownHostException, IOException,
			TuplespaceException, TimeoutException {
		return createProxy(interfaze, classLoader, new Class[] { interfaze },
				id, space, explicit, timeout,
				TuplespaceContainer.DEFAULT_LEASETIME, true);
	}

	@SuppressWarnings("unchecked")
	public static <T> T createProxy(Class<T> returnType,
			ClassLoader classLoader, Class<?>[] interfazes, String id,
			String host, int port, boolean explicit, long timeout,
			long leaseTime, boolean checkInterfaces)
			throws UnknownHostException, IOException, TuplespaceException,
			TimeoutException {

		if (classLoader == null)
			classLoader = Toolbox.findCommonClassLoader(interfazes);
		if (classLoader == null) {
			LOGGER.warn("TuplespaceProxy: warning: cannot find suiting class loader, try default");
			classLoader = TuplespaceProxy.class.getClassLoader();
		}

		TuplespaceInvocationHandler invHandler = new TuplespaceInvocationHandler(
				classLoader, interfazes, id, host, port, explicit, timeout,
				leaseTime, checkInterfaces);

		for (Class<?> c : interfazes)
			invHandler.findObservableType(c);

		return (T) Proxy.newProxyInstance(classLoader, interfazes, invHandler);
	}

	@SuppressWarnings("unchecked")
	public static <T> T createProxy(Class<T> returnType,
			ClassLoader classLoader, Class<?>[] interfazes, String id,
			Tuplespace space, boolean explicit, long timeout, long leaseTime,
			boolean checkInterfaces) throws UnknownHostException, IOException,
			TuplespaceException, TimeoutException {

		if (classLoader == null)
			classLoader = Toolbox.findCommonClassLoader(interfazes);
		if (classLoader == null) {
			LOGGER.warn("TuplespaceProxy: warning: cannot find suiting class loader, try default");
			classLoader = TuplespaceProxy.class.getClassLoader();
		}

		TuplespaceInvocationHandler invHandler = new TuplespaceInvocationHandler(
				classLoader, interfazes, id, space, explicit, timeout,
				leaseTime, checkInterfaces);

		for (Class<?> c : interfazes)
			invHandler.findObservableType(c);

		return (T) Proxy.newProxyInstance(classLoader, interfazes, invHandler);
	}

	/**
	 * closes connection to {@link TuplespacePublisher} server
	 * 
	 * @param proxy
	 *            Proxy whose connection is closed
	 */
	public static void deleteProxy(Object proxy) {
		((TuplespaceInvocationHandler) Proxy.getInvocationHandler(proxy))
				.close();
	}

	/**
	 * class providing generic method invocation forwarding to a
	 * {@link TuplespacePublisher}
	 * 
	 * @author Christian Koch
	 * 
	 */
	private static class TuplespaceInvocationHandler implements
			SerializableNetworkInvocationHandler {
		private Tuplespace space;
		private TuplespaceCommandQueue commandQueue;
		private TuplespaceEventQueue eventQueue;
		private boolean explicit;

		// unused
		// private String id;

		private Hashtable<TypeVariable<?>, GClass> typevarmap;
		// maps type parameters of parameterized classes to
		// actual types used (needed because of type erasure)

		private GenericObservable genericObservable = null;

		private String id;
		private String host;
		private int port;
		private Class<?>[] interfaces;

		private ClassLoader classLoader;

		/**
		 * recursively process given type searching for implementation of
		 * Observable interface and creating the type parameter map
		 * 
		 * @param curtype
		 *            current type to process
		 * @throws TimeoutException
		 * @throws TuplespaceException
		 */
		public void findObservableType(java.lang.reflect.Type curtype)
				throws TuplespaceException, TimeoutException {
			GClass.fromType(curtype, typevarmap);

			GClass[] obsparams = GClass.getGenericsOf(curtype,
					Observable.class, typevarmap);
			if (obsparams != null && obsparams.length == 1) {
				genericObservable = new GenericObservable(obsparams[0],
						classLoader);

				final Object eventNotify = new Object();

				/*
				 * process events in a separate thread, and not in the (network)
				 * thread that notified us
				 */
				Thread eventListenThread = new Thread(new Runnable() {
					@Override
					public void run() {
						while (true) {
							boolean newCommand = false;
							synchronized (eventNotify) {
								try {
									eventNotify.wait();
									newCommand = true;
								} catch (InterruptedException e) {
									LOGGER.warn(
											"TuplespaceProxy eventListenThread interrupted",
											e);
									// warn and go
								}
							}

							if (!newCommand)
								continue;

							processEvents();
						}

					}

				});
				eventListenThread.setName("TuplespaceProxy eventListenThread");
				eventListenThread.setDaemon(true);
				eventListenThread.start();

				eventQueue.addObserver(new Observer<TuplespaceEvent>() {
					@Override
					public void notify(
							Observable<? extends TuplespaceEvent> sender,
							TuplespaceEvent event) {
						synchronized (eventNotify) {
							eventNotify.notifyAll();
						}
					}
				});
				eventQueue.initEventQueueReader();

			}
		}

		/**
		 * creates new invocation handler using given
		 * {@link TuplespacePublisher} host and port
		 * 
		 * @param classLoader
		 * 
		 * @param host
		 *            host name or IP of Tuplespace
		 * @param port
		 *            port number of Tuplespace
		 * @throws UnknownHostException
		 *             specified host not found
		 * @throws IOException
		 *             error while connecting to NetworkPublisher server
		 * @throws TuplespaceException
		 */
		public TuplespaceInvocationHandler(ClassLoader classLoader,
				Class<?>[] interfaces, String id, String host, int port,
				boolean explicit, long timeout, long leaseTime,
				boolean checkInterfaces) throws UnknownHostException,
				IOException, TuplespaceException {
			this.typevarmap = new Hashtable<TypeVariable<?>, GClass>();

			this.id = id;
			this.interfaces = interfaces;
			this.classLoader = classLoader;
			this.explicit = explicit;

			this.host = host;
			this.port = port;
			this.space = NetworkProxy.createProxy(Tuplespace.class, host, port);

			init(checkInterfaces, timeout, leaseTime);
		}

		/**
		 * creates new invocation handler using given
		 * {@link TuplespacePublisher} host and port
		 * 
		 * @param classLoader
		 * 
		 * @param space
		 *            {@link Tuplespace} to use
		 * @throws UnknownHostException
		 *             specified host not found
		 * @throws IOException
		 *             error while connecting to NetworkPublisher server
		 * @throws TuplespaceException
		 */
		public TuplespaceInvocationHandler(ClassLoader classLoader,
				Class<?>[] interfaces, String id, Tuplespace space,
				boolean explicit, long timeout, long leaseTime,
				boolean checkInterfaces) throws UnknownHostException,
				IOException, TuplespaceException {
			this.typevarmap = new Hashtable<TypeVariable<?>, GClass>();

			this.id = id;
			this.interfaces = interfaces;
			this.classLoader = classLoader;
			this.explicit = explicit;

			this.host = null;
			this.port = -1;
			this.space = space;

			init(checkInterfaces, timeout, leaseTime);
		}

		private void init(boolean checkInterfaces, long timeout, long leaseTime)
				throws TuplespaceException {
			if (checkInterfaces) {
				TuplespacePublisherInformationProvider tpip = new TuplespacePublisherInformationProvider(
						space, true, classLoader);
				de.mmis.core.base.Type deviceType = tpip.getDeviceType(id);
				if (deviceType == null)
					throw new IllegalArgumentException(
							"cannot determine device type");
				de.mmis.core.base.Type[] interfaceTypes = tpip
						.getInterfaces(deviceType);
				if (interfaceTypes == null)
					throw new IllegalArgumentException(
							"cannot determine interface types");
				nextinterface: for (Class<?> c : this.interfaces) {
					for (de.mmis.core.base.Type t : interfaceTypes)
						if (c.equals(t.getJavaClass()))
							continue nextinterface;
					throw new IllegalArgumentException("given interface " + c
							+ " is not implemented by the remote object");
				}
			}

			this.commandQueue = new TuplespaceCommandQueue(space, "command",
					Tree.fromObject(id), timeout, leaseTime);
			this.eventQueue = new TuplespaceEventQueue(space, "event",
					Tree.fromObject(id), timeout, leaseTime);
		}

		/**
		 * forward invocation of Proxy method to {@link NetworkPublisher}
		 * 
		 * @param proxy
		 *            Proxy object invocation was made on
		 * @param method
		 *            method invoked
		 * @param args
		 *            array of method parameters
		 * @return result of method call
		 * @throws ProxyException
		 *             if TuplespacePublisher returned error message
		 * @throws Throwable
		 *             any other error that may occur
		 */

		@Override
		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {

			if (genericObservable != null) {
				if (method.getName().equals("addObserver") && args.length == 1) {
					genericObservable.addObserver((Observer<?>) args[0]);
					return null;
				} else if (method.getName().equals("removeObserver")
						&& args.length == 1) {
					genericObservable.removeObserver((Observer<?>) args[0]);
					return null;
				}
			}

			InnerNode funcCall = new InnerNode();
			funcCall.add(new LeafNode(method.getName()));

			if (args != null) {
				for (int i = 0; i < args.length; i++)
					funcCall.add(Tree.fromObject(args[i], explicit));
			}

			long resultId = commandQueue.push(funcCall);

			long timeout = Annotations.getAnnotationTimeout(method, args,
					commandQueue.getTimeout());

			Tree retexp = commandQueue.takeResult(resultId, timeout);

			if (retexp != null
					&& retexp.getType() == Type.INNER
					&& ((InnerNode) retexp).getNumberOfSubTrees() > 0
					&& ((InnerNode) retexp).getSubTree(0).getType() == Type.LEAF
					&& ((LeafNode) ((InnerNode) retexp).getSubTree(0))
							.equalsUTF8String(":error")) {
				throw ProxyException.generateException(retexp, classLoader);
			}

			if (method.getReturnType() == void.class)
				return null;
			Object retval = SerializerHelper.deserializeObject(retexp,
					GClass.fromType(method.getGenericReturnType(), typevarmap),
					classLoader, new HashMap<String, Object>());

			return retval;
		}

		@Override
		public void finalize() {
			close();
		}

		/**
		 * closes {@link NetworkPublisher} socket (only if created by host and
		 * port in constructor)
		 */
		public void close() {
			if (host != null && space != null)
				NetworkProxy.deleteProxy(space);
		}

		protected void processEvents() {
			try {
				// System.out.println("notified of event");
				Tree eventExpression = eventQueue.readNextEvent();
				while (eventExpression != null) {
					// System.out.println("event found");
					genericObservable.fireEvent(eventExpression);
					eventExpression = eventQueue.readNextEvent();
				}
			} catch (TuplespaceException e) {
				LOGGER.error("TuplespaceQueue error while reading next event",
						e);
				// warn and go
			} catch (TimeoutException e) {
				LOGGER.error(
						"TuplespaceQueue error while reading next event: ", e);
				// warn and go
			}
		}

		@Override
		public String getHost() {
			return host;
		}

		@Override
		public Class<?>[] getImplementingInterfaces() {
			return interfaces;
		}

		@Override
		public int getPort() {
			return port;
		}

		@Override
		public Class<?> getProxyCreationClass() {
			return TuplespaceProxy.class;
		}

		@Override
		public String getID() {
			return id;
		}
	}
}
